/* @internal */
namespace ts {
    const visitedNestedConvertibleFunctions = createMap<true>();

    export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): DiagnosticWithLocation[] {
        program.getSemanticDiagnostics(sourceFile, cancellationToken);
        const diags: DiagnosticWithLocation[] = [];
        const checker = program.getTypeChecker();

        visitedNestedConvertibleFunctions.clear();
        check(sourceFile);

        addRange(diags, sourceFile.bindSuggestionDiagnostics);
        addRange(diags, program.getSuggestionDiagnostics(sourceFile, cancellationToken));
        return diags.sort((d1, d2) => d1.start - d2.start);

        function check(node: Node) {
            if (isVariableStatement(node) &&
                node.parent === sourceFile &&
                node.declarationList.flags & NodeFlags.Const &&
                node.declarationList.declarations.length === 1) {
                const init = node.declarationList.declarations[0].initializer;
                if (init) {
                    diags.push(createDiagnosticForNode(init, Diagnostics.require_call_may_be_converted_to_an_import));
                }
            }

            if (isFunctionLikeDeclaration(node)) {
                addConvertToAsyncFunctionDiagnostics(node, checker, diags);
            }
            node.forEachChild(check);
        }
    }

    function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: Push<DiagnosticWithLocation>): void {
        // need to check function before checking map so that deeper levels of nested callbacks are checked
        if (isConvertibleFunction(node, checker) && !visitedNestedConvertibleFunctions.has(getKeyFromNode(node))) {
            diags.push(createDiagnosticForNode(
                !node.name && isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ? node.parent.name : node,
                Diagnostics.This_may_be_converted_to_an_async_function));
        }
    }

    function isConvertibleFunction(node: FunctionLikeDeclaration, checker: TypeChecker) {
        return node.body &&
            isBlock(node.body) &&
            hasReturnStatementWithPromiseHandler(node.body) &&
            returnsPromise(node, checker);
    }

    function returnsPromise(node: FunctionLikeDeclaration, checker: TypeChecker): boolean {
        const functionType = checker.getTypeAtLocation(node);
        const callSignatures = checker.getSignaturesOfType(functionType, SignatureKind.Call);
        const returnType = callSignatures.length ? checker.getReturnTypeOfSignature(callSignatures[0]) : undefined;
        return !!returnType;
    }

    function hasReturnStatementWithPromiseHandler(body: Block): boolean {
        return !!forEachReturnStatement(body, isReturnStatementWithFixablePromiseHandler);
    }

    export function isReturnStatementWithFixablePromiseHandler(node: Node): node is ReturnStatement {
        return isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression);
    }

    // Should be kept up to date with transformExpression in convertToAsyncFunction.ts
    export function isFixablePromiseHandler(node: Node): boolean {
        // ensure outermost call exists and is a promise handler
        if (!isPromiseHandler(node) || !node.arguments.every(isFixablePromiseArgument)) {
            return false;
        }

        // ensure all chained calls are valid
        let currentNode = node.expression;
        while (isPromiseHandler(currentNode) || isPropertyAccessExpression(currentNode)) {
            if (isCallExpression(currentNode) && !currentNode.arguments.every(isFixablePromiseArgument)) {
                return false;
            }
            currentNode = currentNode.expression;
        }
        return true;
    }

    function isPromiseHandler(node: Node): node is CallExpression {
        return isCallExpression(node) && (hasPropertyAccessExpressionWithName(node, "then") || hasPropertyAccessExpressionWithName(node, "catch"));
    }

    // should be kept up to date with getTransformationBody in convertToAsyncFunction.ts
    function isFixablePromiseArgument(arg: Expression): boolean {
        switch (arg.kind) {
            case SyntaxKind.FunctionDeclaration:
            case SyntaxKind.FunctionExpression:
            case SyntaxKind.ArrowFunction:
                visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as FunctionLikeDeclaration), true);
            // falls through
            case SyntaxKind.NullKeyword:
            case SyntaxKind.Identifier: // identifier includes undefined
                return true;
            default:
                return false;
        }
    }

    function getKeyFromNode(exp: FunctionLikeDeclaration) {
        return `${exp.pos.toString()}:${exp.end.toString()}`;
    }
}
